跳到主要内容

ES6 的迭代器和生成器

大量参考 ES6系列---迭代器(Iterator)与生成器(Generator) 参考资料 ES6 迭代器

循环语句的问题

var colors = ["red", "green", "blue"];
for(var i=0; i<colors.length; i++){
console.log(colors[i]);
}

在 ES6 之前,这种标准的 for 循环,通过变量来跟踪数组的索引(变量 i)。如果多个循环嵌套就需要追踪多个变量,代码复杂度会大大增加,也容易产生错用循环变量的 bug。

什么是迭代器

用 ES5 语法模拟创建一个迭代器

function createIterator(items) {
var i = 0;

return { // 返回一个迭代器对象
next: function() { // 迭代器对象一定有个next()方法
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;

return { // next()方法返回结果对象
value: value,
done: done
};
}
};
}

var iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); // "{ value: 1, done: false}"
console.log(iterator.next()); // "{ value: 2, done: false}"
console.log(iterator.next()); // "{ value: 3, done: false}"
console.log(iterator.next()); // "{ value: undefiend, done: true}"
// 之后所有的调用都会返回相同内容
console.log(iterator.next()); // "{ value: undefiend, done: true}"

借助这个迭代器对象,来改造刚开始那个标准的 for 循环

var colors = ["red", "green", "blue"];
var iterator = createIterator(colors);
while(!iterator.next().done){
console.log(iterator.next().value);
}

什么是生成器

生成器是一种返回迭代器的函数,通过 function 关键字后的星号(*)来表示,函数中会用到新的关键字 yield。

function *createIterator(items) {
for(let i=0; i<items.length; i++) {
yield items[i];
}
}

let iterator = createIterator([1, 2, 3]);

// 既然生成器返回的是迭代器,自然就可以调用迭代器的next()方法
console.log(iterator.next()); // "{ value: 1, done: false}"
console.log(iterator.next()); // "{ value: 2, done: false}"
console.log(iterator.next()); // "{ value: 3, done: false}"
console.log(iterator.next()); // "{ value: undefiend, done: true}"
// 之后所有的调用都会返回相同内容
console.log(iterator.next()); // "{ value: undefiend, done: true}"

用 ES6 的生成器,大大简化了迭代器的创建过程。给生成器函数 createIterator() 传入一个 items 数组,函数内部,for 循环不断从数组中生成新的元素放入迭代器中,每遇到一个 yield 语句循环都会停止;每次调用迭代器的 next() 方法,循环便继续运行并停止在下一条 yield 语句处。

下面来看下生成器是怎么创建的

ES6 Generator 生成器

ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。

这个就和 Unity 里使用的协程用法是一致的

Generator 有两个区分于普通函数的部分:

  • function 后面,函数名之前有个 *
  • 函数内部有 yield 表达式。

其中 * 用来表示函数为 Generator 函数,yield 用来定义函数内部的状态。

下面是它的创建方式

// 生成器是个函数
function* func(){
console.log("one");
yield '1';
console.log("two");
yield '2';
console.log("three");
return '3';
}

// 生成器内部也可以通过 for 循环不断从数组中生成新的元素放入迭代器中
function *createIterator(items) {
for(let i=0; i<items.length; i++) {
yield items[i];
}
}


// 可以用函数表达式方式书写:
let createIterator = function *(item) { ... }


// 也可以添加到对象中
let o = {
createIterator: function *(items) { ... }
};

let iterator = o.createIterator([1, 2, 3]);


// ES6风格的对象方法简写方式:
let o = {
*createIterator(items) { ... }
};

let iterator = o.createIterator([1, 2, 3]);

可迭代(Iterable)对象

在 ES6 中,所有的集合对象(数组、Set集合及Map集合)和字符串都是可迭代对象,可迭代对象都绑定了默认的迭代器。

var colors = ["red", "green", "blue"];
for(let color of colors){
console.log(color);
}

for-of 循环,可作用在可迭代对象上,正是利用了可迭代对象上的默认迭代器。大致过程是:for-of 循环每执行一次都会调用可迭代对象的 next() 方法,并将迭代器返回的结果对象的 value 属性存储在变量中,循环将继续执行这一过程直到返回对象的 done 属性的值为 true。

如果只需要迭代数组或集合中的值,可以用 for-of 循环代替 for循环

生成器执行机制

调用 Generator 函数和调用普通函数一样,在函数名后面加上 () 即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,所以要调用遍历器对象 Iteratornext 方法,指针就会从函数头部或者上一次停下来的地方开始执行。

注意:除了使用 next,还可以使用 for... of 循环遍历 Generator 函数生产的 Iterator 对象。

function* func() {
console.log("one");
yield '1';
console.log("two");
yield '2';
console.log("three");
return '3';
}

let f = func();

/**
* 第一次调用 next 方法时,从 Generator 函数的头部开始执行,
* 先是打印了 one ,执行到 yield 就停下来,并将 yield 后边表
* 达式的值 '1',作为返回对象的 value 属性值,此时函数还没有
* 执行完, 返回对象的 done 属性值是 false。
*/
console.log(f.next());
// one
// {value: "1", done: false}


/* 第二次调用 next 方法时,同上步 */
console.log(f.next());
// two
// {value: "2", done: false}

/**
* 第三次调用 next 方法时,先是打印了 three ,然后执行了函数
* 的返回操作,并将 return 后面的表达式的值,作为返回对象的
* value 属性值,此时函数已经结束,多以 done 属性值为true 。
*/
console.log(f.next());
// three
// {value: "3", done: true}

/**
* 第四次调用 next 方法时, 此时函数已经执行完了,所以返回
* value 属性值是 undefined ,done 属性值是 true 。如果执
* 行第三步时,没有 return 语句的话,就直接返回 {value: undefined, done: true}。
*/
console.log(f.next());
// {value: undefined, done: true}

访问默认迭代器

可迭代对象,都有一个 Symbol.iterator 方法,for-of 循环时,通过调用 colors 数组的 Symbol.iterator 方法来获取默认迭代器的(这一过程是在 JavaScript 引擎背后完成的)

let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();

console.log(iterator.next()); // "{ value: 1, done: false}"
console.log(iterator.next()); // "{ value: 2, done: false}"
console.log(iterator.next()); // "{ value: 3, done: false}"
console.log(iterator.next()); // "{ value: undefined, done: true}"

在这段代码中,通过 Symbol.iterator 获取了数组 values 的默认迭代器,并用它遍历数组中的元素。在 JavaScript 引擎中执行 for-of 循环语句也是类似的处理过程。

用 Symbol.iterator 属性来检测对象是否为可迭代对象:

function isIterator(object) {
return typeof object[Symbol.iterator] === "function";
}

console.log(isIterable([1, 2, 3])); // true
console.log(isIterable(new Set())); // true
console.log(isIterable(new Map())); // true
console.log(isIterable("Hello")); // true

创建可迭代对象

在创建对象时,给 Symbol.iterator 属性添加一个生成器,则可以将其变成可迭代对象:

let collection = {
items: [],
*[Symbol.iterator]() { // 将生成器赋值给对象的 Symbol.iterator 属性来创建默认的迭代器
for(let item of this.items) {
yield item;
}
}
};

collection.items.push(1);
collection.items.push(2);
collection.items.push(3);

for(let x of collection) {
console.log(x);
}

不同集合的默认迭代器

ES6中的集合对象,数组、Set集合和 Map集合,都内建了三种迭代器:

  • entries() 返回一个迭代器,其值为多个键值对。如果是数组,第一个元素是索引位置;如果是 Set 集合,第一个元素与第二个元素一样,都是值。
  • values() 返回一个迭代器,其值为集合的值。
  • keys() 返回一个迭代器,其值为集合中的所有键名。如果是数组,返回的是索引;如果是Set集合,返回的是值(Set的值被同时用作键和值)。

每个集合类型都有一个默认的迭代器,在 for-of 循环中,如果没有显式指定则使用默认的迭代器。按常规使用习惯,数组和 Set 集合的默认迭代器是 values(),Map 集合的默认迭代器是 entries()

let colors = [ "red", "green", "blue"];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "print");

// 与调用colors.values()方法相同
for(let value of colors) {
console.log(value);
}

// 与调用tracking.values()方法相同
for(let num of tracking) {
console.log(num);
}

// 与调用data.entries()方法相同
for(let entry of data) {
console.log(entry);
}

返回值为;

"red"
"green"
"blue"
1234
5678
9012
["title", "Understanding ECMAScript 6"]
["format", "print"]

for 配合解构

for-of 循环配合解构特性,操纵数据会更方便:

for(let [key, value] of data) {
console.log(key + "=" + value);
}

用展开运算符操纵

let set = new Set([1, 2, 3, 4, 5]),
array = [...set];

console.log(array); // [1,2,3,4,5]

展开运算符可以操作所有的可迭代对象,并根据默认迭代器来选取要引用的值,从迭代器读取所有值。然后按返回顺序将它们依次插入到数组中。因此如果想将可迭代对象转换为数组,用展开运算符是最简单的方法。

next 方法返回值

next() 方法:一般情况下,next 方法不传入参数的时候,yield 表达式的返回值是 undefined。当 next 传入参数的时候,该参数会作为上一步 yield 的返回值。

function* sendParameter(){
console.log("start");
var x = yield '2';

console.log("one:" + x);
var y = yield '3';

console.log("two:" + y);
console.log("total:" + (x + y));
}

next 不传参

var sendp1 = sendParameter();
sendp1.next();
// start
// {value: "2", done: false}
sendp1.next();
// one:undefined
// {value: "3", done: false}
sendp1.next();
// two:undefined
// total:NaN
// {value: undefined, done: true}

next 传参

var sendp2 = sendParameter();
sendp2.next(10);
// start
// {value: "2", done: false}
sendp2.next(20);
// one:20
// {value: "3", done: false}
sendp2.next(30);
// two:30
// total:50
// {value: undefined, done: true}

结束函数 return 方法

注意:这个是 return 方法,不是 return 关键字

return 方法返回给定值,并结束遍历 Generator 函数。同上,该方法提供参数时,返回该参数;不提供参数时,返回 undefined

function* foo(){
yield 1;
yield 2;
yield 3;
}
var f = foo();
f.next();
// {value: 1, done: false}
f.return("foo");
// {value: "foo", done: true}
f.next();
// {value: undefined, done: true}

抛出异常 throw 方法

注意:这个是 throw 方法,不是 throw 关键字

throw 方法可以在 Generator 函数体外面抛出异常,在函数体内部捕获。

如下例子:遍历器对象抛出了两个错误,第一个被 Generator 函数内部捕获,第二个因为函数体内部的 catch 函数已经执行过了,不会再捕获这个错误,所以这个错误就抛出 Generator 函数体,被函数体外的 catch 捕获。

var g = function* () {
try {
yield;
} catch (e) {
console.log('catch inner', e);
}
};

var i = g();
i.next();

try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('catch outside', e);
}
// catch inner a
// catch outside b

yield* 表达式

yield* 表达式表示 yield 返回一个遍历器对象,用于在 Generator 函数内部,调用另一个 Generator 函数。

function* callee() {
console.log('callee: ' + (yield));
}
function* caller() {
while (true) {
yield* callee();
}
}

const callerObj = caller();
callerObj.next();
// {value: undefined, done: false}
callerObj.next("a");
// callee: a
// {value: undefined, done: false}
callerObj.next("b");
// callee: b
// {value: undefined, done: false}

等同于

function* caller() {
while (true) {
for (var value of callee) {
yield value;
}
}
}